策略权限控制:类 MongoDB 的复杂查询逻辑
基础 conditions 只能处理简单属性匹配,面对多字段组合条件(如"非私密且非本人文章可读")就力不从心。CASL 通过 buildMongoQueryMatcher 工厂函数支持 MongoDB 查询操作符,结合 @ucast/mongo2js 库可实现 $or、$nor、$in 等复杂条件逻辑,并且支持将这些条件序列化为 JSON 字符串存入数据库。
从基础条件到复杂查询
基础条件的局限
// 基础条件:只能匹配单个属性值
can('read', 'Article', { authorId: 1 });
// 无法表达:authorId 不等于 1 且 private 不为 true
typescript
MongoDB 查询操作符的引入
CASL 官方提供了 buildMongoQueryMatcher 工厂函数,默认支持部分 MongoDB 操作符。通过 @ucast/mongo2js 库可扩展支持所有操作符:
import {
createMongoAbility,
AbilityBuilder,
buildMongoQueryMatcher,
} from '@casl/ability';
import { $nor, nor } from '@ucast/mongo2js';
// 扩展支持 $nor 操作符
const conditionsMatcher = buildMongoQueryMatcher({ $nor }, { nor });
typescript
$nor 操作符实战
$nor 对多个条件表达式执行逻辑 NOR 操作 -- 文档不满足数组中的任何一个条件时才匹配:
function defineAbilityFor(user: any) {
const { can, build } = new AbilityBuilder(createMongoAbility);
can('read', 'Article', {
$nor: [
{ private: true }, // 条件 1:不是私密的
{ authorId: user.id } // 条件 2:不是本人文章
]
});
return build({ conditionsMatcher });
}
typescript
条件判断逻辑
| Article 属性 | 是否满足 $nor 条件 | 可读性 |
|---|---|---|
{ authorId: 1, private: false } + user.id=2 | 不满足条件1 (false) 且 不满足条件2 (true) = NOR 不通过 | 不可读 |
{ authorId: 2, private: false } + user.id=1 | 不满足条件1 且 不满足条件2 = NOR 通过 | 可读 |
{ authorId: 2, private: true } + user.id=1 | 满足条件1 (true) = NOR 不通过 | 不可读 |
$nor要求所有条件都为 false 时整体才为 true。即"这些条件中任何一个都不满足"。
支持所有 MongoDB 操作符
ucast 库的 instructions 和 interpreters
默认的 buildMongoQueryMatcher 只包含部分操作符。@ucast/core 库提供了完整的操作符支持:
npm install @ucast/mongo2js
bash
import { allParsingInstructions, allInterpreters } from '@ucast/mongo2js';
// 导入所有操作符支持
const conditionsMatcher = buildMongoQueryMatcher(
allParsingInstructions,
allInterpreters
);
typescript
支持的操作符示例
| 操作符 | 含义 | 示例 |
|---|---|---|
$or | 任一条件满足 | { $or: [{ a: 1 }, { b: 2 }] } |
$nor | 所有条件都不满足 | { $nor: [{ a: 1 }, { b: 2 }] } |
$in | 值在数组中 | { status: { $in: ['active', 'pending'] } } |
$lt/$gt | 小于/大于 | { age: { $gt: 18 } } |
$regex | 正则匹配 | { name: { $regex: '^admin' } } |
JSON 字符串序列化
条件对象可序列化为 JSON 字符串存入数据库,读取后反序列化使用:
// 序列化:存入数据库
const conditionStr = JSON.stringify({
$or: [
{ authorId: 1 },
{ private: true }
]
});
// 反序列化:从数据库读取后使用
const conditions = JSON.parse(conditionStr);
// CASL 完全兼容反序列化后的对象
can('read', 'Article', conditions);
typescript
完整的条件匹配器配置
import {
createMongoAbility,
AbilityBuilder,
buildMongoQueryMatcher,
} from '@casl/ability';
import { allParsingInstructions, allInterpreters } from '@ucast/mongo2js';
// 创建支持所有 MongoDB 操作符的条件匹配器
const conditionsMatcher = buildMongoQueryMatcher(
allParsingInstructions,
allInterpreters
);
function defineAbilityFor(user: any) {
const { can, build } = new AbilityBuilder(createMongoAbility);
// $or: authorId 为 1 或 private 为 true 的文章可读
can('read', 'Article', {
$or: [
{ authorId: user.id },
{ private: false }
]
});
return build({ conditionsMatcher });
}
typescript
数据库存储设计思考
将 MongoDB 查询条件存入数据库时,需要考虑:
- 条件存储:将 conditions 对象序列化为 JSON 字符串存储
- 类型区分:通过
type字段区分 JSON 条件(0)、MongoDB 查询(1)和函数条件(2) - 反序列化:从数据库读取后通过
JSON.parse还原为对象
下一节将介绍第三种条件策略 -- 基于函数的条件匹配,以及如何将这三种策略统一整合。
↑